package bancosys.tec.services.api;

import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import jmine.tec.di.scope.TypedScope;
import jmine.tec.persist.PersistMessages;
import jmine.tec.services.ServicesController;
import jmine.tec.services.annotations.ServiceFields;
import jmine.tec.services.api.ActionsEnum;
import jmine.tec.services.api.Service;
import jmine.tec.services.api.ServiceAssertionFailedError;
import jmine.tec.services.api.ServiceExecutionException;
import jmine.tec.services.api.ServiceProperties;
import jmine.tec.services.api.ServiceValidationExecutionException;
import jmine.tec.services.api.ServicesAPIMessages;
import jmine.tec.services.api.ServicesPage;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.SpringTransactionAnnotationParser;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.base.enumx.EnumUtils;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.exception.NaturalKeyNotDefinedException;
import bancosys.tec.utils.date.Date;
import bancosys.tec.utils.date.DateUtils;

/**
 * Implementação padrão de um serviço.
 * 
 * @author Rafael Volpato (Jun 4, 2007)
 * @param <C> Controller principal utilizado pelo serviço
 */
public abstract class AbstractService<C extends PersistenceEnabledController> implements Service<C> {

    private C controller;

    private ServicesController servicesController;

    private TypedScope serviceProperties;

    private ServicesPage servicesPage;

    private String serviceLocation;

    /**
     * Construtor.
     */
    public AbstractService() {
        // do nothing for now.
    }

    /**
     * Construtor.
     * 
     * @param serviceProperties service properties
     */
    public AbstractService(ServiceProperties serviceProperties) {
        this.serviceProperties = serviceProperties;
    }

    /**
     * @return o controller
     * @deprecated olhar interface {@link Service}
     */
    @Deprecated
    public C getController() {
        return this.controller;
    }

    /**
     * @param controller controller
     * @deprecated olhar interface {@link Service}
     */
    @Deprecated
    public void setController(C controller) {
        this.controller = controller;
    }

    /**
     * @return the servicesController
     */
    public ServicesController getServicesController() {
        return this.servicesController;
    }

    /**
     * @param servicesController the servicesController to set
     */
    public void setServicesController(ServicesController servicesController) {
        this.servicesController = servicesController;
    }

    /**
     * @return serviceProperties
     */
    public TypedScope getServiceProperties() {
        return this.serviceProperties;
    }

    /**
     * @param serviceProperties serviceProperties
     */
    public void setServiceProperties(TypedScope serviceProperties) {
        this.serviceProperties = serviceProperties;
    }

    /**
     * Valida se todos os parametros obrigatorios foram informados.
     * 
     * @return lista de erros
     */
    public List<String> validate() {
        List<String> errors = new ArrayList<String>();

        if (!this.getClass().isAnnotationPresent(ServiceFields.class)) {
            throw new ServiceAssertionFailedError(ServicesAPIMessages.ANNOTATION_SERVICE_MISSING_ANNOTATION.create(this.getClass()
                    .getName()));
        }

        ServiceFields serviceFields = this.getClass().getAnnotation(ServiceFields.class);
        for (String field : serviceFields.required()) {
            if (field.startsWith("$")) { // Não verificar se existe um valor
                // setado, apenas se a chave existe
                if (!this.serviceProperties.containsKey(field)) {
                    errors.add("O campo '" + field + "' é obrigatório (não foi definido)");
                }
            } else {
                Object fieldValue = null;
                try {
                    fieldValue = this.serviceProperties.get(field, Object.class);

                    if (fieldValue == null) {
                        errors.add("O campo '" + field + "' é obrigatório");
                    } else if (fieldValue instanceof String && ((String) fieldValue).trim().equals("")) {
                        errors.add("O campo '" + field + "' é obrigatório (não pode ser vazio)");
                    }
                } catch (ServiceValidationExecutionException e) {
                    errors.add("Erro ao tentar obter o valor do campo  '" + field + "'");
                }
            }
        }

        return errors;
    }

    /**
     * Retorna a action desse servico.
     * 
     * @return a action desse servico
     */
    public ActionsEnum getAction() {

        if (!this.getClass().isAnnotationPresent(ServiceFields.class)) {
            throw new ServiceAssertionFailedError(ServicesAPIMessages.ANNOTATION_SERVICE_MISSING_ANNOTATION.create(this.getClass()
                    .getName()));
        }

        ServiceFields serviceFields = this.getClass().getAnnotation(ServiceFields.class);

        return serviceFields.action();
    }

    /**
     * Retorna o nome do servico.
     * 
     * @return o nome do servico
     */
    public String getServiceName() {
        return getServiceName(this.getClass());
    }

    /**
     * Retorna o nome do serviço dado sua classe.
     * 
     * @param clazz Classe de serviço
     * @return o nome do serviço
     */
    public static String getServiceName(Class<?> clazz) {
        String className = clazz.getName();
        String serviceName = className.substring(className.lastIndexOf('.') + 1);

        serviceName = serviceName.substring(0, serviceName.lastIndexOf("Service"));

        return serviceName;
    }

    /**
     * @return the serviceLocation
     */
    public String getServiceLocation() {
        return this.serviceLocation;
    }

    /**
     * @param serviceLocation the serviceLocation to set
     */
    public void setServiceLocation(String serviceLocation) {
        this.serviceLocation = serviceLocation;
    }

    /**
     * Define pagina (aba) desse servico.
     * 
     * @param servicePage pagina desse servico.
     * @see Service#setServicesPage(ServicesPage)
     */
    public void setServicesPage(ServicesPage servicePage) {
        this.servicesPage = servicePage;
    }

    /**
     * Devolve pagina (aba) desse servico.
     * 
     * @return pagina (aba) desse servico.
     * @see Service#getServicesPage()
     */
    public ServicesPage getServicesPage() {
        return this.servicesPage;
    }

    /**
     * Retorna a instancia do Service corrente na forma de String.
     * 
     * @return a instancia do Service corrente na forma de String
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        sb.append("[");

        if (this.getServiceLocation() != null) {
            sb.append(this.getServiceLocation());
            sb.append(":");
        } else if (this.getServicesPage() != null) {
            sb.append(this.getServicesPage().getName());
            sb.append(":");
        }

        sb.append(this.getAction());
        sb.append(".");
        sb.append(this.getServiceName());

        sb.append("] ");

        sb.append(this.getServiceProperties().toString());

        return sb.toString();
    }

    /*
     * Metodos auxiliares para o tratamento dos service properties
     */
    /**
     * Atalho para pegar propriedade.
     * 
     * @param <T> classe que vai ser retornada
     * @param key propriedade
     * @param clazz classe que deve ser retornada
     * @return valor da propriedade
     */
    protected <T> T getProperty(String key, Class<T> clazz) {
        return this.getServiceProperties().get(key, clazz);
    }

    /**
     * Atalho para obter o valor de uma propriedade que é uma String (forma mais utilizada).
     * 
     * @param key propriedade
     * @return valor da propriedade
     */
    protected String getProperty(String key) {
        return this.getProperty(key, String.class);
    }

    /**
     * Atalho para obter uma propriedade na forma de um objeto date.
     * 
     * @param key property
     * @return BigDecimal
     */
    protected Date getDateProperty(String key) {
        return DateUtils.getCalendarByViewDateDDMMYYYY(this.getProperty(key));
    }

    /**
     * Atalho para obter uma propriedade na forma de um objeto Integer.
     * 
     * @param key property
     * @return Integer
     */
    protected Integer getIntegerProperty(String key) {
        return this.getBigDecimalProperty(key).intValue();
    }

    /**
     * Atalho para obter uma propriedade na forma de um objeto Long.
     * 
     * @param key property
     * @return Long
     */
    protected Long getLongProperty(String key) {
        return Long.valueOf(this.getProperty(key));
    }

    /**
     * Atalho para obter uma propriedade na forma de um objeto boolean.
     * 
     * @param key property
     * @return boolean
     */
    protected boolean getBoolean(String key) {
        String property = this.getProperty(key);
        return property.toUpperCase().equals("SIM") || property.toUpperCase().equals("TRUE");
    }

    /**
     * Atalho para obter uma propriedade na forma de um bigDecimal.
     * 
     * @param key propriedade
     * @return BigDecimal
     */
    protected BigDecimal getBigDecimalProperty(String key) {
        return new BigDecimal(this.getProperty(key));
    }

    /**
     * Devolve enum de java para o valor passado.
     * 
     * @param key chave.
     * @param <E> tipo de enum.
     * @param enumClass classe do enum.
     * @return enum.
     */
    protected <E extends Enum<E>> E getJavaEnumProperty(final Class<E> enumClass, final String key) {
        return EnumUtils.getEnumByValue(enumClass, key);
    }

    /**
     * Devolve enum do apache commons para o valor passado.
     * 
     * @param key chave.
     * @param <E> tipo de enum.
     * @param enumClass classe do enum.
     * @return enum.
     */
    protected <E extends org.apache.commons.lang.enums.Enum> E getApacheEnumProperty(final Class<E> enumClass, final String key) {
        return EnumUtils.getApacheEnumByValue(enumClass, key);
    }

    /**
     * Retorna uma entidade a partir do nome da propriedade que possui sua NaturalKey. O valor pode ser referenciado (com '$') ou uma
     * String. Caso o valor não seja uma referencia, a entidade <code>clazz</code> deve possuir a anotação <code>@NaturalKey</code>.
     * 
     * @param <T> persistable
     * @param naturalKeyField nome da propriedade (a constante que identifica o campo)
     * @param clazz classe da entidade sendo recuperada
     * @return uma entidade a partir do nome da propriedade que possui sua NaturalKey.
     * @throws ServiceExecutionException não encontre nenhuma entidade, ou a entidade não possua a anotação <code>@NaturalKey</code>
     */
    @SuppressWarnings("deprecation")
    public <T extends Persistable> T getByNaturalKey(String naturalKeyField, Class<T> clazz) throws ServiceExecutionException {
        String naturalKey = this.getProperty(naturalKeyField);
        T entity;

        if (naturalKey.startsWith("$")) {
            entity = this.getProperty(naturalKeyField, clazz);
        } else {
            BaseDAO<T> dao = this.getController().getDAOFactory().getGenericDAO(clazz);

            try {
                entity = dao.findByNaturalKey(naturalKey);
            } catch (NaturalKeyNotDefinedException e) {
                throw new ServiceExecutionException(e.getLocalizedMessageHolder(), e);
            } catch (BeanNotFoundException e) {
                throw new ServiceExecutionException(PersistMessages.BEAN_NOT_FOUND_BY_NATURAL_KEY.create(clazz.getName(), naturalKey), e);
            }
        }
        return entity;
    }

    /**
     * Grava uma variavel que no service properties.
     * 
     * @param key chave da propriedade (com ou sem o "$" no inicio da String)
     * @param reference objeto que deve ser referenciado
     */
    protected void saveReference(String key, Object reference) {
        if (this.getServiceProperties().containsKey(key)) {
            String keyPropertyValue = this.getProperty(key);

            // COM o "$" no inicio da String
            String referenceKey = keyPropertyValue.startsWith("$") ? keyPropertyValue : "$" + keyPropertyValue;
            this.getServiceProperties().putDefinition(referenceKey, reference);
        }
    }

    /**
     * Cria uma copia do serviço.
     * 
     * @return uma copia do serviço.
     * @throws CloneNotSupportedException se não foi possivel criar uma copia do serviço
     */
    @Override
    @SuppressWarnings("unchecked")
    public Service<C> clone() throws CloneNotSupportedException {
        AbstractService<C> clone = (AbstractService<C>) super.clone();
        clone.serviceProperties = null;
        clone.servicesPage = null;
        clone.serviceLocation = null;
        return clone;
    }

    /**
     * Retorna a classe do controller, obtida atravez de generics.
     * 
     * @return a classe do controller.
     * @deprecated olhar interface {@link Service}
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public Class<C> getControllerClass() {
        try {
            TypeVariable<Class<AbstractService>>[] variables = AbstractService.class.getTypeParameters();
            return (Class<C>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(variables[0], this.getClass());
        } catch (IllegalArgumentException e) {
            throw new ServiceAssertionFailedError(ServicesAPIMessages.INVALID_SERVICE_IMPLEMENTATION.create(this.getClass().getName()), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public TransactionDefinition getTransactionDefinition() {
        return new SpringTransactionAnnotationParser().parseTransactionAnnotation(this.getClass());
    }
}